using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using System.Web;

namespace Tyng.MediaWiki
{
    
    public sealed class MediaWikiApi
    {
        private const string HiddenInputRegex = "type=['\"]hidden['\"] value=['\"](?<value>[^'\"]*?)['\"] name=['\"]{0}['\"]";
        private const string HiddenInputRegexAlt = "type=['\"]hidden['\"] name=['\"]{0}['\"] value=['\"](?<value>[^'\"]*?)['\"]";
        private const string ApiPath = "/w/api.php";
        private const string EditPath = "/w/index.php?title={0}&action=edit";
        private const string DefaultMediaWikiPath = "http://en.wikipedia.org";

        string _basePath = DefaultMediaWikiPath;
        string _token;
        string _username;
        int _userId;
        bool _postDataAlways;
        bool _isAnonymous;
        string _sessionId;

        private MediaWikiApi() : this(null) { }

        private MediaWikiApi(string basePath)
        {
            SetBasePath(basePath);
            
            _isAnonymous = true;
        }

        public MediaWikiApi(string basePath, string user, string password)
        {
            SetBasePath(basePath);

            Login(user, password, null);
        }

        public MediaWikiApi(string user, string password) : this(null, user, password) { }

        private void SetBasePath(string passed)
        {
            //TODO: app settings...
            if (string.IsNullOrEmpty(passed)) 
                _basePath = DefaultMediaWikiPath;
            else
                _basePath = passed;
        }

        private static MediaWikiApi _anonymous;
        public static MediaWikiApi Anonymous
        {
            get
            {
                if (_anonymous == null)
                    _anonymous = new MediaWikiApi();

                return _anonymous;
            }
        }

        public bool AlwaysPostData
        {
            get
            {
                return _postDataAlways;
            }
            set
            {
                _postDataAlways = value;
            }
        }

        public bool IsAnonymous
        {
            get
            {
                return _isAnonymous;
            }
        }

        private void Login(string user, string password, string domain)
        {
            if (_isAnonymous) throw new InvalidOperationException("Cannot login from anonymous session.");

            string[] parameters;

            user = Uri.EscapeDataString(user);
            password = Uri.EscapeDataString(password);

            if (string.IsNullOrEmpty(domain))
                parameters = new string[] { "lgname=" + user, "lgpassword=" + password };
            else
            {
                domain = Uri.EscapeDataString(domain);
                parameters = new string[] { "lgname=" + user, "lgpassword=" + password, "lgdomain=" + domain };
            }

            XmlDocument xml = RequestApi("login", true, parameters);

            XmlElement result = (XmlElement) xml.SelectSingleNode("/api/login");
            
            string resultValue = result.Attributes["result"].Value;

            switch(resultValue)
            {
                case "Success":
                    _userId = int.Parse(result.Attributes["lguserid"].Value);
                    _token = result.Attributes["lgtoken"].Value;
                    _username = result.Attributes["lgusername"].Value;
                    break;
                case "NoName":
                    throw new LoginException("Empty name.", resultValue);
                case "Illegal":
                    throw new LoginException("Illegal login.", resultValue);
                case "WrongPluginPass":
                    throw new LoginException("Incorrect plugin password.", resultValue);
                case "NotExists":
                    throw new LoginException("User does not exist.", resultValue);
                case "WrongPass":
                    throw new LoginException("Incorrect password.", resultValue);
                case "EmptyPass":
                    throw new LoginException("Empty password.", resultValue);
                default:
                    throw new LoginException("An unexpected login error occurred.", resultValue);
            }
        }

        private string GetHiddenInputValue(string name, string html)
        {
            Match m = Regex.Match(html, string.Format(HiddenInputRegex, name));
            if (!m.Success) m = Regex.Match(html, string.Format(HiddenInputRegexAlt, name));
            if (!m.Success) return null;

            return m.Groups["value"].Value;
        }

        internal XmlDocument EditPage(string title, string newContent, string editSummary, bool minorEdit)
        {
            string url = CombineUrlPath(_basePath, string.Format(EditPath, Uri.EscapeDataString(title)));

            //TODO: move to API once its implemented there
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.AllowAutoRedirect = false;
            request.UserAgent = "Tyng.MediaWikiApi Library";
            request.Method = "GET";

            CookieCollection cookies = new CookieCollection();
            cookies.Add(new Cookie("enwikiUserID", _userId.ToString(), "/", "en.wikipedia.org"));
            cookies.Add(new Cookie("enwikiToken", _token, "/", "en.wikipedia.org"));
            cookies.Add(new Cookie("enwikiUserName", _username, "/", "en.wikipedia.org"));
            if (!string.IsNullOrEmpty(_sessionId)) cookies.Add(new Cookie("enwiki_session", _sessionId, "/", "en.wikipedia.org"));

            request.CookieContainer = new CookieContainer();
            request.CookieContainer.Add(cookies);

            Sleep("query");
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();

            if (response.StatusCode != HttpStatusCode.OK)
                throw new EditException(response);

            if(response.Cookies["enwiki_session"] != null)
                _sessionId = response.Cookies["enwiki_session"].Value;

            string editToken = null;
            string editTime = null;
            string startTime = null;

            using (StreamReader sr = new StreamReader(response.GetResponseStream()))
            {
                string html = sr.ReadToEnd();
                editToken = GetHiddenInputValue("wpEditToken", html);
                editTime = GetHiddenInputValue("wpEdittime", html);
                startTime = GetHiddenInputValue("wpStarttime", html);
            }

            request = (HttpWebRequest)WebRequest.Create(url);
            request.AllowAutoRedirect = false;
            request.UserAgent = "Tyng.MediaWikiApi Library";
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";

            cookies = new CookieCollection();
            cookies.Add(new Cookie("enwikiUserID", _userId.ToString(), "/", "en.wikipedia.org"));
            cookies.Add(new Cookie("enwikiToken", _token, "/", "en.wikipedia.org"));
            cookies.Add(new Cookie("enwikiUserName", _username, "/", "en.wikipedia.org"));
            cookies.Add(new Cookie("enwiki_session", _sessionId, "/", "en.wikipedia.org"));

            request.CookieContainer = new CookieContainer();
            request.CookieContainer.Add(cookies);

            using (StreamWriter sw = new StreamWriter(request.GetRequestStream()))
            {
                sw.Write("{0}={1}", "wpEdittime", Uri.EscapeDataString(editTime));
                sw.Write("&{0}={1}", "wpStarttime", Uri.EscapeDataString(startTime));
                sw.Write("&{0}={1}", "wpEditToken", Uri.EscapeDataString(editToken));
                sw.Write("&{0}={1}", "wpSave", "Save Page");

                if (minorEdit) sw.Write("&{0}={1}", "wpMinoredit", "1");

                sw.Write("&{0}={1}", "wpTextbox1", Uri.EscapeDataString(newContent));
                sw.Write("&{0}={1}", "wpSummary", Uri.EscapeDataString(editSummary));
            }

            Sleep("edit");

            response = (HttpWebResponse)request.GetResponse();
            if (response.StatusCode == HttpStatusCode.Redirect)
            {
                //probably successful, check url
                string redirect = response.Headers[HttpResponseHeader.Location];
            }
            else
            {
                throw new EditException(response);
            }

            XmlElement page;
            do
            {
                //TODO: once the edit api is implemented and it returns xml, can get rid of this...
                XmlDocument xml = api.RequestApi("query", "prop=info", "titles=" + Uri.EscapeDataString(title));

                //poll it until page is ready...
                page = (XmlElement)xml.SelectSingleNode("/api/query/pages/page");

            } while (page == null || page.Attributes["missing"] != null);            
        }

        public Page GetPage(string title)
        {
            return GetPage(new string[] { title })[0];
        }

        public Page GetPage(int id)
        {
            return GetPage(new int[] { id })[0];
        }

        public Page[] GetPage(params string[] titles)
        {
            string escapedTitles = string.Join("|", titles);
            escapedTitles = Uri.EscapeDataString(escapedTitles);

            XmlDocument xml = RequestApi("query", "prop=info|revisions", "titles=" + escapedTitles);

            XmlNode pages = xml.SelectSingleNode("/api/query/pages");

            List<Page> normalized = new List<Page>(titles.Length);

            foreach (XmlNode page in pages.ChildNodes)
            {
                normalized.Add(new Page((XmlElement)page));
            }

            return normalized.ToArray();
        }

        public Page[] GetPage(params int[] pageIds)
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < pageIds.Length; i++)
            {
                if (i > 0) sb.Append("|");
                sb.Append(pageIds[i].ToString());
            }

            XmlDocument xml = RequestApi("query", "prop=info|revisions", "pageids=" + sb.ToString());

            XmlNode pages = xml.SelectSingleNode("/api/query/pages");

            List<Page> normalized = new List<Page>(pageIds.Length);

            foreach (XmlNode page in pages.ChildNodes)
            {
                normalized.Add(new Page((XmlElement)page));
            }

            return normalized.ToArray();
        }

        internal XmlDocument RequestApi(string action, params string[] parameters)
        {
            return RequestApi(action, _postDataAlways, parameters);
        }

        internal XmlDocument RequestApi(string action, bool postData, params string[] parameters)
        {
            UriBuilder uri = new UriBuilder(CombineUrlPath(_basePath, ApiPath));

            StringBuilder sb = new StringBuilder("format=xml&action=");
            
            sb.Append(action);

            for(int i = 0; i < parameters.Length; i++)
            {
                sb.Append("&");
                sb.Append(parameters[i]);
            }

            if (!_isAnonymous && !string.IsNullOrEmpty(_username) && !string.IsNullOrEmpty(_token))
            {
                sb.AppendFormat("&lgtoken={0}&lgusername={1}&lguserid={2}", _token, _username, _userId);
            }

            if (!postData)  uri.Query = sb.ToString();

            WebRequest request = WebRequest.Create(uri.Uri);

            if (postData)
            {
                request.ContentType = "application/x-www-form-urlencoded";
                request.Method = "POST";

                using (StreamWriter sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.Write(sb.ToString());
                }
            }
            else
            {
                request.Method = "GET";
            }

            Sleep(action);

            WebResponse response = request.GetResponse();
            
            using (StreamReader sr = new StreamReader(response.GetResponseStream()))
            {
                string fullText = sr.ReadToEnd();
                XmlDocument xml = new XmlDocument();
                xml.LoadXml(fullText);
                return xml;
            }
        }

        private static string CombineUrlPath(string basePath, string relativePath)
        {
            if (string.IsNullOrEmpty(basePath)) throw new ArgumentNullException("basePath");
            if (string.IsNullOrEmpty(relativePath)) return basePath;

            if ((relativePath.StartsWith("/") && !basePath.EndsWith("/")) || (!relativePath.StartsWith("/") && basePath.EndsWith("/")))
                return basePath + relativePath;

            if (relativePath.StartsWith("/") && basePath.EndsWith("/"))
                return basePath + relativePath.Substring(1);

            if (!relativePath.StartsWith("/") && !basePath.EndsWith("/"))
                return basePath + "/" + relativePath;

            //not sure it can reach here, but maybe...
            throw new ArgumentException("Arguments in invalid format");
        }

        #region Request Timers
        static Dictionary<string, AutoResetEvent> _syncs = new Dictionary<string, AutoResetEvent>();

        private static void Sleep(string sleepType)
        {
            if (!Config.Sleep.ContainsKey(sleepType)) throw new ArgumentException("Unexpected sleep type", "sleepType");

            int sleepTime = Config.Sleep.GetSleep(sleepType);

            if (sleepTime == 0) return;

            AutoResetEvent reset = null;
            lock (_syncs)
            {
                if (!_syncs.TryGetValue(sleepType, out reset))
                {
                    reset = new AutoResetEvent(true);
                    _syncs.Add(sleepType, reset);
                }
            }

            reset.WaitOne();
            ThreadPool.QueueUserWorkItem(delegate(object state) { Thread.Sleep(sleepTime); reset.Set(); });
        }
        #endregion
    }
}
